/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.eclipse.ui;

import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.browser.IWebBrowser;

import net.sourceforge.pmd.eclipse.ui.editors.StyleExtractor;
import net.sourceforge.pmd.eclipse.ui.editors.SyntaxData;
import net.sourceforge.pmd.eclipse.ui.editors.SyntaxManager;
import net.sourceforge.pmd.eclipse.ui.preferences.editors.SWTUtil;
import net.sourceforge.pmd.eclipse.util.FontBuilder;
import net.sourceforge.pmd.lang.Language;

/**
 * 
 * @author Brian Remedios
 */
public class PageBuilder {
    private static final char CR = '\n';
    private static final Color BACKGROUND = Display.getDefault().getSystemColor(SWT.COLOR_WHITE);
    
    private static final Comparator<StyleRange> STYLE_COMPARATOR = new Comparator<StyleRange>() {
        @Override
        public int compare(StyleRange sr1, StyleRange sr2) {
            return sr1.start - sr2.start;
        }
    };

    private List<int[]> headingSpans = new ArrayList<>();
    private List<int[]> codeSpans = new ArrayList<>();
    private Map<int[], String> linksBySpan = new HashMap<>();

    private StringBuilder buffer;

    private Color headingColor;
    private int indentDepth;
    private TextStyle codeStyle;
    private StyleExtractor codeStyleExtractor;


    public PageBuilder(int textIndent, int headingColorIndex, FontBuilder codeFontBuilder) {
        buffer = new StringBuilder(500);
        indentDepth = textIndent;

        Display display = Display.getDefault();
        headingColor = display.getSystemColor(headingColorIndex);
        codeStyle = codeFontBuilder.style(display);

        SyntaxData syntax = SyntaxManager.getSyntaxData("java");
        codeStyleExtractor = new StyleExtractor(syntax);
    }

    public static StyleRange[] sort(List<StyleRange> ranges) {
        StyleRange[] styles = ranges.toArray(new StyleRange[0]);
        Arrays.sort(styles, STYLE_COMPARATOR);
        return styles;
    }

    public void indentDepth(int aDepth) {
        indentDepth = aDepth;
    }

    public int indentDepth() {
        return indentDepth;
    }

    public boolean hasLinks() {
        return !linksBySpan.isEmpty();
    }

    public void clear() {

        buffer.setLength(0);
        if (headingSpans != null) {
            headingSpans.clear();
        }
        if (codeSpans != null) {
            codeSpans.clear();
        }
        if (linksBySpan != null) {
            linksBySpan.clear();
        }
    }

    public void setLanguage(Language language) {

        SyntaxData syntax = SyntaxManager.getSyntaxData(language.getTerseName());
        codeStyleExtractor.syntax(syntax);
    }

    public void addText(String text) {

        for (int i = 0; i < indentDepth; i++) {
            buffer.append(' ');
        }
        buffer.append(text).append(CR);
    }

    public void addRawText(String text) {

        buffer.append(text);
    }

    public void addHeading(String headingKey) {

        String heading = SWTUtil.stringFor(headingKey);

        int length = buffer.length();
        if (length > 0) {
            buffer.append(CR);
            length += 1;
        }

        headingSpans.add(new int[] { length, length + heading.length() });
        buffer.append(heading).append(CR);
    }

    public void addCode(String code) {

        int length = buffer.length();

        codeSpans.add(new int[] { length, length + code.length() });
        buffer.append(code);
    }

    public void addLink(String text, String link) {

        int length = buffer.length();

        linksBySpan.put(new int[] { length, length + text.length() }, link);

        buffer.append(text);
    }

    private String linkAt(int textIndex) {

        int[] span;

        for (Map.Entry<int[], String> entry : linksBySpan.entrySet()) {
            span = entry.getKey();
            if (span[0] <= textIndex && textIndex <= span[1]) {
                return entry.getValue();
            }
        }

        return null;
    }

    public void showOn(StyledText widget) {

        String text = buffer.toString();

        widget.setText(text);

        List<StyleRange> ranges = new ArrayList<>();

        int[] span;

        for (int i = 0; i < headingSpans.size(); i++) {
            span = headingSpans.get(i);
            ranges.add(new StyleRange(span[0], span[1] - span[0], headingColor, BACKGROUND, SWT.BOLD));
        }

        for (int[] spn : linksBySpan.keySet()) {
            StyleRange style = new StyleRange(spn[0], spn[1] - spn[0], headingColor, BACKGROUND, SWT.UNDERLINE_LINK);
            style.underline = true;
            ranges.add(style);
        }

        String crStr = Character.toString(CR);
        StyleRange sr;

        for (int i = 0; i < codeSpans.size(); i++) {
            span = codeSpans.get(i);
            sr = new StyleRange(codeStyle);
            sr.start = span[0];
            sr.length = span[1] - span[0];
            // ranges.add(sr); TODO wtf? causes crashes

            List<StyleRange> colorRanges = codeStyleExtractor.stylesFor(text, sr.start, sr.length, crStr);
            for (StyleRange range : colorRanges) {
                ranges.add(range);
            }
        }

        StyleRange[] styles = sort(ranges); // must be in order!
        widget.setStyleRanges(styles);
    }

    public void addLinkHandler(final StyledText widget) {

        widget.addListener(SWT.MouseDown, new Listener() {
            @Override
            public void handleEvent(Event event) {
                // It is up to the application to determine when and how a link
                // should be activated.
                // In this snippet links are activated on mouse down when the
                // control key is held down
                // if ((event.stateMask & SWT.MOD1) != 0) {
                try {
                    int offset = widget.getOffsetAtLocation(new Point(event.x, event.y));
                    String link = linkAt(offset);
                    if (link != null) {
                        launchBrowser(link);
                    }
                } catch (IllegalArgumentException ignored) {
                    // no character under event.x, event.y
                }

            }
            // }
        });

    }

    private static void launchBrowser(String link) {
        try {
            IWebBrowser browser = PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser();
            browser.openURL(new URL(link));
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}
